import path from '../../node/path'
import mkdirs from '../mkdirs'
import pathExists from '../path-exists'
import stat from '../util/stat'
import fs from '../fs/index'
import errorCode from '../../node/error-code'
import { checkError } from '../../universalify'

export default function copy (src, dest, opts?, cb?) {
    if (typeof opts === 'function' && !cb) {
        cb = opts
        opts = {}
    } else if (typeof opts === 'function') {
        opts = { filter: opts }
    }

    cb = cb || function () {}
    opts = opts || {}

    opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
    opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber


    stat.checkPaths(src, dest, 'copy', opts, (err, stats) => {
        if (checkError(err)) return cb(err)
        const { srcStat, destStat } = stats
        stat.checkParentPaths(src, srcStat, dest, 'copy', err => {
            if (checkError(err)) return cb(err)
            if (opts.filter) return runFilter(checkParentDir, destStat, src, dest, opts, cb);
            return checkParentDir(destStat, src, dest, opts, cb);
        })
    })
}

function checkParentDir (destStat, src, dest, opts, cb) {
    const destParent = path.dirname(dest)
    pathExists.pathExists(destParent, (err, dirExists) => {
        if (checkError(err)) return cb(err)
        if (dirExists) return getStats(destStat, src, dest, opts, cb)
        mkdirs.mkdirs(destParent, err => {
            if (checkError(err)) return cb(err)
            return getStats(destStat, src, dest, opts, cb)
        })
    })
}

function runFilter (onInclude, destStat, src, dest, opts, cb) {
    Promise.resolve(opts.filter(src, dest))
        .then(include => {
        if (include) return onInclude(destStat, src, dest, opts, cb);
        return cb();
    }, error => cb(error))
}

function statCopy(destStat, src, dest, opts, cb) {
    if (opts.filter) return runFilter(getStats, destStat, src, dest, opts, cb);
    return getStats(destStat, src, dest, opts, cb);
}

function getStats(destStat, src, dest, opts, cb) {
    const stat = opts.dereference ? fs.stat : fs.lstat
    stat(src, (err, srcStat) => {
        if (checkError(err)) return cb(err)

        if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb)
        else if (srcStat.isFile() ||
                 srcStat.isCharacterDevice() ||
                 srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb)
        else if (srcStat.isSymbolicLink()) return cb(new Error(`Cannot copy a socket file:${src}`))
        return cb(new Error(`Unknown file: ${src}`))
    })
}

function onFile (srcStat, destStat, src, dest, opts, cb) {
    if (!destStat) return copyFile(srcStat, src, dest, opts, cb)
    return mayCopyFile(srcStat, src, dest, opts, cb)
}

function mayCopyFile (srcStat, src, dest, opts, cb) {
    if (opts.overwrite) {
        fs.unlink(dest, err => {
            if (checkError(err)) return cb(err)
            return copyFile(srcStat, src, dest, opts, cb)
        })
    } else if (opts.errorOnExist) {
        return cb(new Error(`'${dest}' already exists`))
    } else return cb()
}

function copyFile (srcStat, src, dest, opts, cb) {
    fs.copyFile(src, dest, err => {
        if (checkError(err)) return cb(err)
        return setDestMode(dest, srcStat.mode, cb)
    })
}

function fileIsNotWritable (srcMode) {
    return (srcMode & 0o200) === 0
}

function makeFileWritable (dest, srcMode, cb) {
    return setDestMode(dest, srcMode | 0o200, cb)
}

function setDestMode (dest, srcMode, cb) {
    return fs.chmod(dest, srcMode, cb)
}

function onDir (srcStat, destStat, src, dest, opts, cb) {
    if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts, cb)
    return copyDir(src, dest, opts, cb)
}

function mkDirAndCopy (srcMode, src, dest, opts, cb) {
    fs.mkdir(dest, err => {
        if (checkError(err)) return cb(err)
        copyDir(src, dest, opts, err => {
            if (checkError(err)) return cb(err)
            return setDestMode(dest, srcMode, cb)
        })
    })
}

function copyDir (src, dest, opts, cb) {
    fs.readdir(src, (err, items) => {
        if (checkError(err)) return cb(err)
        return copyDirItems(items, src, dest, opts, cb)
    })
}

function copyDirItems (items, src, dest, opts, cb) {
    const item = items.pop()
    if (!item) return cb()
    return copyDirItem(items, item, src, dest, opts, cb)
}

function copyDirItem (items, item, src, dest, opts, cb) {
    const srcItem = path.join(src, item)
    const destItem = path.join(dest, item)
    stat.checkPaths(srcItem, destItem, 'copy', opts, (err, stats) => {
        if (checkError(err)) return cb(err)
        const { destStat } = stats
        statCopy(destStat, srcItem, destItem, opts, err => {
            if (checkError(err)) return cb(err)
            return copyDirItems(items, src, dest, opts, cb)
        })
    })
}

function onLink (destStat, src, dest, opts, cb) {
    fs.readlink(src, (err, resolvedSrc) => {
        if (checkError(err)) return cb(err)
        if (opts.dereference) {
            resolvedSrc = path.resolve('/', resolvedSrc)
        }

        if (!destStat) {
            return fs.symlink(resolvedSrc, dest, cb)
        } else {
            fs.readlink(dest, (err, resolvedDest) => {
                if (checkError(err)) return cb(err)
                // dest exists and is a regular file or directory,
                // Windows may throw UNKNOWN error. If dest already exists,
                // fs throws error anyway, so no need to guard against it here.
                if (err.message === errorCode.errormessage.EINVAL) return fs.symlink(resolvedSrc, dest, cb)
                return cb(err)

                if (opts.dereference) {
                    resolvedDest = path.resolve('/', resolvedDest)
                }

                if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
                    return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`))
                }

                // do not copy if src is a subdir of dest since unlinking
                // dest in this case would result in removing src contents
                // and therefore a broken symlink would be created.
                if (destStat.isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
                    return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
                }
                return copyLink(resolvedSrc, dest, cb)
            })
        }
    })
}

function copyLink (resolvedSrc, dest, cb) {
    fs.unlink(dest, err => {
        if (checkError(err)) return cb(err)
        return fs.symlink(resolvedSrc, dest, cb)
    })
}